How metric recording works with diginsight and Opentelemetry

πŸ“‘ Table of Contents

Understanding Diginsight Metrics

Diginsight provides automatic span duration metrics collection that seamlessly integrates with OpenTelemetry. These metrics measure the execution time of operations (activities/spans) throughout your application.

What are Diginsight Metrics?

Diginsight metrics are automatically collected through MetricRecorder classes, with the primary recorder being SpanDurationMetricRecorder. This recorder:

  • Listens to activity lifecycle events using the .NET ActivityListener mechanism
  • Automatically records duration when activities complete
  • Exports metrics via OpenTelemetry to your monitoring backend (Application Insights, Prometheus, etc.)

Key metric: - diginsight.span_duration: Records the duration in milliseconds of each activity/span with tags like: - span_name: The operation name - status: Activity status (Ok, Error, etc.) - Custom tags from activity attributes

Example:

public async Task<Order> ProcessOrderAsync(int orderId)
{
    // Diginsight automatically creates instrumented activity
    using var activity = ActivitySource.StartMethodActivity(new { orderId });
    
    // Your business logic executes
    var order = await GetOrderFromDatabase(orderId);
    await ValidateInventory(order);
    await ProcessPayment(order);
    
    // when activity stops, metrics are automatically recorded:
    // diginsight.span_duration{span_name="ProcessOrderAsync", status="Ok", orderId="123"} = 250ms
    
    return order;
}

How Metrics Are Sent

Metrics flow through this pipeline:

Activity Creation β†’ ActivityStopped Event β†’ SpanDurationMetricRecorder 
    β†’ OpenTelemetry Metrics Pipeline β†’ Exporters (App Insights, Prometheus, etc.)

Startup Configuration:

// Register Diginsight metrics collection
builder.Services.AddSpanDurationMetricRecorder();

// Configure OpenTelemetry to export metrics
builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
        .AddMeter("Diginsight.Diagnostics")  // Listen to Diginsight metrics
        .AddPrometheusExporter()             // Export to Prometheus
        .AddApplicationInsightsExporter());  // Export to Azure

Configuration Architecture: Shared and Metric-Specific Settings

Diginsight’s configuration follows a two-layer architecture that separates generic activity tracking settings from metric-specific configuration:

Layer 1: Shared Activity Tracking (Generic)

These settings apply to ALL metrics and activity tracking across your application:

{
  "Diginsight": {
    "Activities": {
      // βœ… SHARED - applies to all metrics
      "MeterName": "MyApp.Telemetry",  // Generic meter for all metrics
      
      "ActivitySources": {
        "MyApp.*": true,
        "Azure.Cosmos.*": true,
        "System.Net.Http": true
      },
      
      "LoggedActivityNames": {
        "Expensive.Operation": "Hide",
        "Debug.OnlyOp": "Hide"
      }
    }
  }
}

Key shared properties: - MeterName: Generic OpenTelemetry meter name used by all metrics (default: application name) - ActivitySources: Which activity sources to monitor (applies to all metrics and logging) - LoggedActivityNames: Which activities to log (shared logging configuration)

Layer 2: Metric-Specific Settings

Each metric can have its own recording flag, metric name, and optionally override the meter name:

{
  "Diginsight": {
    "Activities": {
      "MeterName": "MyApp.Telemetry",  // Shared meter
      
      // Span Duration Metric Configuration
      "RecordSpanDuration": true,
      "SpanDurationMetricName": "diginsight.span_duration",
      "SpanDurationMetricDescription": "Duration of application spans",
      
      // Future: Query Cost Metric Configuration (example)
      // "RecordQueryCost": true,
      // "QueryCostMetricName": "diginsight.query_cost",
      // "QueryCostMetricDescription": "Cosmos DB query cost in RUs"
    }
  }
}

Configuration Precedence

Meter Name Resolution:

SpanDurationMeterName (specific) β†’ MeterName (generic) β†’ Exception (if neither set)

Example scenarios:

// βœ… Recommended: Use generic MeterName
{
  "MeterName": "MyApp.Telemetry"  // All metrics use this
}

// βœ… Backward Compatible: SpanDurationMeterName still works
{
  "SpanDurationMeterName": "MyApp.Telemetry"  // Only span_duration uses this
}

// βœ… Mixed: Generic + specific override
{
  "MeterName": "MyApp.Telemetry",           // Default for all metrics
  "SpanDurationMeterName": "MyApp.Spans"    // Override for span_duration only
}

Per-Metric Filters and Enrichers

Filters and enrichers can have default configuration that applies to all metrics, plus metric-specific overrides:

{
  "Diginsight": {
    "Activities": {
      "MeterName": "MyApp.Telemetry",
      
      // Default filter: applies to all metrics
      "SpanMeasuredActivityNames": {
        "*": true  // Measure everything by default
      },
      
      // Metric-specific overrides
      "MetricSpecificSpanMeasuredActivityNames": [
        {
          "MetricName": "diginsight.span_duration",
          "ActivityNames": {
            "FastOperation.*": false  // Exclude from span_duration
          }
        },
        {
          "MetricName": "diginsight.query_cost",
          "ActivityNames": {
            "Operation.query_*": true,  // Only queries for query_cost
            "*": false
          }
        }
      ],
      
      // Default enricher tags: applies to all metrics
      "MetricTags": [
        "category_name",
        "user_company"
      ],
      
      // Metric-specific tag overrides
      "MetricSpecificTags": [
        {
          "MetricName": "diginsight.span_duration",
          "MetricTags": ["response_status"]  // Additional tags for span_duration
        },
        {
          "MetricName": "diginsight.query_cost",
          "MetricTags": ["database_name", "partition_key"]  // Additional tags for query_cost
        }
      ]
    }
  }
}

Custom Metrics with Shared Configuration

When creating custom metrics (like query cost), you can leverage the shared configuration:

public class QueryCostRecorder : IActivityListenerLogic
{
    private readonly Lazy<Histogram<double>> lazyMetric;
    private readonly IMetricRecordingFilter? metricFilter;
    private readonly IMetricRecordingEnricher? metricEnricher;
    
    public QueryCostRecorder(
        IClassAwareOptions<DiginsightActivitiesOptions> activitiesOptionsMonitor,
        IMeterFactory meterFactory,
        IServiceProvider serviceProvider)
    {
        string metricName = "diginsight.query_cost";
        
        // Reuse DiginsightActivitiesOptions for meter configuration
        this.lazyMetric = new Lazy<Histogram<double>>(() =>
        {
            var options = activitiesOptionsMonitor.CurrentValue;
            // Uses generic MeterName or falls back to SpanDurationMeterName
            return meterFactory.Create(options.MeterName)
                               .CreateHistogram<double>(
                                   metricName,
                                   "RU",
                                   "Cosmos DB query cost in Request Units");
        });
        
        // Reuse shared filter/enricher infrastructure with metric-specific configuration
        var filter = serviceProvider.GetNamedService<IMetricRecordingFilter>(metricName);
        this.metricFilter = filter ?? serviceProvider.GetRequiredService<IMetricRecordingFilter>();
        
        var enricher = serviceProvider.GetNamedService<IMetricRecordingEnricher>(metricName);
        this.metricEnricher = enricher ?? serviceProvider.GetRequiredService<IMetricRecordingEnricher>();
    }
    
    public void ActivityStopped(Activity activity)
    {
        if (activity.GetTagItem("db.cosmosdb.request_charge") is not double requestCharge)
            return;
            
        Histogram<double> metric = lazyMetric.Value;
        
        // Apply filter (respects metric-specific configuration)
        if (!(metricFilter?.ShouldRecord(activity, metric) ?? true))
            return;
        
        // Build tags with enricher (respects metric-specific tags)
        TagList tags = new()
        {
            { "operation", activity.GetTagItem("db.operation") },
            { "database", activity.GetTagItem("db.name") }
        };
        
        metricEnricher?.Enrich(activity, tags);
        
        metric.Record(requestCharge, tags);
    }
}

Benefits of this architecture: - βœ… Shared activity tracking configuration - one ActivitySources for all metrics - βœ… Reusable meter configuration - one MeterName for all metrics - βœ… Per-metric control - independent recording flags, metric names, filters, and enrichers - βœ… Backward compatible - existing SpanDurationMeterName configurations still work - βœ… Easy to extend - adding new metrics doesn’t duplicate configuration

Tags and Low Cardinality

Metrics can include tags (dimensions) that enable powerful filtering and aggregation in monitoring dashboards. However, tags must have low cardinality to avoid telemetry storage explosion.

Why Low Cardinality Matters

High cardinality (many unique values) causes: - ❌ Storage explosion: Each unique tag combination creates a new time series - ❌ Performance degradation: Query times increase exponentially - ❌ Cost increases: Monitoring systems charge based on unique time series

Examples:

Tag Type Cardinality Suitable? Reason
customer_tier (premium, standard, free) Low βœ… Yes 3 possible values
region (us-east, us-west, eu-west) Low βœ… Yes ~10 possible values
operation_type (read, write, delete) Low βœ… Yes 3-5 possible values
customer_id (UUID per customer) High ❌ No Millions of values
order_id (UUID per order) High ❌ No Unlimited values
request_id (UUID per request) High ❌ No Unlimited values

How to Add Tags Properly

Tags are added through IMetricRecordingEnricher implementations. Diginsight provides:

  1. OptionsBasedMetricRecordingEnricher: Configures tags via appsettings.json
  2. Custom enrichers: Implement IMetricRecordingEnricher for dynamic tagging

Best practices: - βœ… Use categorical values (status, tier, region, environment) - βœ… Use bucketed values (order_value_bucket: small/medium/large instead of exact amounts) - βœ… Keep unique combinations < 1000 per metric - ❌ Avoid user IDs, order IDs, request IDs as tags - ❌ Avoid timestamps, URLs, or freeform text

Filtering Metrics with IMetricRecordingFilter

Not all activities need metrics. Filtering reduces costs and noise by recording only relevant operations.

The IMetricRecordingFilter Interface

public interface IMetricRecordingFilter
{
    bool? ShouldRecord(Activity activity, Instrument instrument);
}

Return values: - true: Force recording - false: Prevent recording - null: Defer to default configuration

OptionsBasedMetricRecordingFilter

Filters activities based on patterns in configuration.

Configuration:

{
  "OptionsBasedMetricRecordingFilter": {
    "ActivityNames": {
      "MyApp.Orders.*": true,           // Record all order operations
      "MyApp.Database.*": true,         // Record database operations
      "Microsoft.AspNetCore.*": false,  // Exclude framework activities
      "System.*": false                 // Exclude system activities
    }
  }
}

Registration:

builder.Services.AddSingleton<IMetricRecordingFilter, OptionsBasedMetricRecordingFilter>();

How it works: 1. When an activity stops, the filter checks its Source.Name and OperationName 2. Matches against configured patterns using wildcards (*) 3. Returns true (record) or false (skip) based on first match

HttpHeadersSpanDurationMetricRecordingFilter

Enables dynamic filtering via HTTP headers - perfect for production debugging!

Use case: Enable metrics for specific requests without redeploying:

# Send request with header to enable metrics for this request
curl -H "Activity-Span-Recording: true" https://myapi.com/api/orders/123

How it works:

public class HttpHeadersSpanDurationMetricRecordingFilter : IMetricRecordingFilter
{
    public const string HeaderName = "Activity-Span-Recording";
    
    public virtual bool? ShouldRecord(Activity activity, Instrument instrument)
    {
        // Check if current HTTP request has the special header
        var httpContext = httpContextAccessor.HttpContext;
        if (httpContext?.Request.Headers.TryGetValue(HeaderName, out var value) == true)
        {
            return bool.TryParse(value, out var result) && result;
        }
        return null; // Defer to other filters
    }
}

Registration:

builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<IMetricRecordingFilter, HttpHeadersSpanDurationMetricRecordingFilter>();

Benefits: - βœ… On-demand metrics for troubleshooting specific requests - βœ… No deployment required - toggle via HTTP headers - βœ… Safe for production - only affects requests with the header - βœ… Fine-grained control - per-request basis

Enriching Metrics with IMetricRecordingEnricher

Enrichers add contextual tags to metrics automatically by extracting relevant information from activities and their context.

The IMetricRecordingEnricher Interface

public interface IMetricRecordingEnricher
{
    Tags ExtractTags(Activity activity, Instrument instrument);
}

Purpose: Extract business-relevant tags from activities to enrich metrics.

Parameters: - activity: The activity that just completed - contains operation context and tags - instrument: The specific metric instrument (e.g., Histogram<double>) being recorded to - enables instrument-specific tag extraction

Return Type: Tags (alias for IEnumerable<KeyValuePair<string, object?>>) - a collection of key-value pairs to add as metric dimensions.

Version History: - v3.6 and earlier: IDictionary<string, object?> ExtractTags(Activity activity) - v3.7.0: Changed to Tags ExtractTags(Activity activity) for better performance - v3.7.1.0: Added Instrument parameter: Tags ExtractTags(Activity activity, Instrument instrument) to enable instrument-specific tag extraction

How Enrichers Work: Tag Flow Pipeline

Enrichers are called automatically when metrics are recorded. Here’s the complete flow:

1. Activity Stops
   ↓
2. MetricRecorder.ActivityStopped() triggered
   ↓
3. Filter checks: ShouldRecord(activity, instrument)?
   ↓ [if true]
4. Build base TagList (operation name, status, etc.)
   ↓
5. Enricher.ExtractTags(activity, instrument) called
   ↓
6. Add enricher tags to TagList
   ↓
7. instrument.Record(value, tagList)
   ↓
8. Metric exported to backend (App Insights, Prometheus, etc.)

Example in SpanDurationMetricRecorder:

public void ActivityStopped(Activity activity)
{
    Histogram<double> metric = lazyMetric.Value;
    
    // Step 3: Apply filter
    if (!(metricFilter?.ShouldRecord(activity, metric) ?? true))
        return;
    
    // Step 4: Build base tags
    var tags = new TagList
    {
        { "span_name", activity.OperationName },
        { "status", activity.Status.ToString() }
    };
    
    // Step 5-6: Call enricher to add additional tags
    if (metricEnricher != null)
    {
        var enrichedTags = metricEnricher.ExtractTags(activity, metric);
        foreach (var tag in enrichedTags)
        {
            tags.Add(tag.Key, tag.Value);
        }
    }
    
    // Step 7: Record metric with all tags
    metric.Record(activity.Duration.TotalMilliseconds, tags);
}

OptionsBasedMetricRecordingEnricher

Configures which activity tags should become metric tags via configuration.

Configuration:

{
  "OptionsBasedMetricRecordingEnricher": {
    "MetricTags": [
      "customer_tier",
      "region",
      "deployment_environment",
      "service_version"
    ]
  }
}

How it works:

public virtual Tags ExtractTags(Activity activity, Instrument instrument)
{
    // Gets configured tag names from options
    var tagNames = options.Value.MetricTags;
    
    // Searches activity hierarchy for matching tags
    return tagNames
        .Select(tagName => {
            // Look in current activity and parent activities
            var value = activity.GetAncestors(includeSelf: true)
                .Select(a => a.GetTagItem(tagName))
                .FirstOrDefault(v => v != null);
            
            return new KeyValuePair<string, object?>(tagName, value);
        })
        .Where(tag => tag.Value != null);
}

Example: Tag Extraction Flow

// Step 1: Create activity with tags
using var activity = ActivitySource.StartRichActivity("ProcessOrder", new
{
    customer_tier = "premium",  // βœ… Will become metric tag (in config)
    region = "us-east",         // βœ… Will become metric tag (in config)
    order_id = "12345",         // ❌ Not in config, won't be included
    order_value = 249.99        // ❌ Not in config, won't be included
});

// ... business logic executes ...

// Step 2: Activity stops, enricher extracts tags
// ExtractTags is called automatically by SpanDurationMetricRecorder
// Returns: [("customer_tier", "premium"), ("region", "us-east")]

// Step 3: Tags added to metric
// Resulting metric exported:
// diginsight.span_duration{
//   span_name="ProcessOrder",
//   customer_tier="premium",
//   region="us-east",
//   status="Ok"
// } = 150ms

Key Benefits: - βœ… Configuration-driven: Change tags without code changes - βœ… Hierarchy search: Finds tags in parent activities too - βœ… Null-safe: Only includes tags that have values - βœ… Low cardinality: You control which tags are included

Registration:

builder.Services.AddSingleton<IMetricRecordingEnricher, OptionsBasedMetricRecordingEnricher>();

Custom Enricher Examples

Create custom enrichers for advanced scenarios beyond configuration-based tagging.

Example 1: Business Context Enricher

public class BusinessContextEnricher : IMetricRecordingEnricher
{
    public Tags ExtractTags(Activity activity, Instrument instrument)
    {
        var tags = new List<KeyValuePair<string, object?>>();
        
        // Add deployment context from environment
        var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
        if (environment != null)
        {
            tags.Add(new KeyValuePair<string, object?>("environment", environment));
        }
        
        // Add version information
        var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString();
        if (version != null)
        {
            tags.Add(new KeyValuePair<string, object?>("version", version));
        }
        
        // Bucket high-cardinality values to maintain low cardinality
        if (activity.GetTagItem("order_value") is double value)
        {
            var bucket = value switch
            {
                < 50 => "small",
                < 200 => "medium",
                < 1000 => "large",
                _ => "enterprise"
            };
            tags.Add(new KeyValuePair<string, object?>("order_value_bucket", bucket));
        }
        
        return tags;
    }
}

Example 2: Instrument-Specific Enricher

public class InstrumentAwareEnricher : IMetricRecordingEnricher
{
    public Tags ExtractTags(Activity activity, Instrument instrument)
    {
        var tags = new List<KeyValuePair<string, object?>>();
        
        // Add different tags based on which metric is being recorded
        switch (instrument.Name)
        {
            case "diginsight.span_duration":
                // For duration metrics, add performance-related tags
                tags.Add(new KeyValuePair<string, object?>("operation_type", 
                    activity.GetTagItem("operation_type")));
                break;
                
            case "diginsight.query_cost":
                // For query cost metrics, add database-related tags
                tags.Add(new KeyValuePair<string, object?>("database", 
                    activity.GetTagItem("db.name")));
                tags.Add(new KeyValuePair<string, object?>("collection", 
                    activity.GetTagItem("db.cosmosdb.container")));
                break;
                
            case "http.request.size":
                // For HTTP metrics, add route information
                tags.Add(new KeyValuePair<string, object?>("http.route", 
                    activity.GetTagItem("http.route")));
                break;
        }
        
        return tags;
    }
}

Example 3: Complete Custom Metric with Enricher

Here’s a complete example showing how to use enrichers in a custom metric recorder:

public class CosmosQueryCostRecorder : IActivityListenerLogic
{
    private readonly Lazy<Histogram<double>> lazyMetric;
    private readonly IMetricRecordingFilter? metricFilter;
    private readonly IMetricRecordingEnricher? metricEnricher;
    
    public CosmosQueryCostRecorder(
        IClassAwareOptions<DiginsightActivitiesOptions> activitiesOptions,
        IMeterFactory meterFactory,
        IServiceProvider serviceProvider)
    {
        string metricName = "diginsight.query_cost";
        
        this.lazyMetric = new Lazy<Histogram<double>>(() =>
        {
            var meter = meterFactory.Create(activitiesOptions.CurrentValue.MeterName);
            return meter.CreateHistogram<double>(
                metricName,
                "RU",
                "Cosmos DB query cost in Request Units");
        });
        
        // Get named enricher for this specific metric, or fall back to default
        this.metricEnricher = serviceProvider.GetNamedService<IMetricRecordingEnricher>(metricName)
                           ?? serviceProvider.GetService<IMetricRecordingEnricher>();
        
        this.metricFilter = serviceProvider.GetNamedService<IMetricRecordingFilter>(metricName)
                         ?? serviceProvider.GetService<IMetricRecordingFilter>();
    }
    
    public void ActivityStopped(Activity activity)
    {
        // Only record for CosmosDB operations that have a query cost
        if (activity.GetTagItem("db.cosmosdb.request_charge") is not double cost || cost <= 0)
            return;
        
        Histogram<double> metric = lazyMetric.Value;
        
        // Apply filter
        if (!(metricFilter?.ShouldRecord(activity, metric) ?? true))
            return;
        
        // Build base tags
        var tags = new TagList
        {
            { "operation", activity.GetTagItem("db.operation") },
            { "database", activity.GetTagItem("db.name") },
            { "container", activity.GetTagItem("db.cosmosdb.container") }
        };
        
        // βœ… Call enricher to add additional tags
        if (metricEnricher != null)
        {
            var enrichedTags = metricEnricher.ExtractTags(activity, metric);
            foreach (var tag in enrichedTags)
            {
                tags.Add(tag.Key, tag.Value);
            }
        }
        
        // Record metric with all tags
        metric.Record(cost, tags);
    }
}

Usage:

// Register the enricher
builder.Services.AddSingleton<IMetricRecordingEnricher, InstrumentAwareEnricher>();

// Register the custom recorder
builder.Services.AddSingleton<IActivityListenerLogic, CosmosQueryCostRecorder>();

// Configure OpenTelemetry to export the metrics
builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
        .AddMeter("MyApp.Telemetry")  // Your configured MeterName
        .AddPrometheusExporter());

Resulting metrics:

diginsight.query_cost{
  operation="Query",
  database="OrdersDB",
  container="Orders",
  environment="Production",
  version="1.2.3"
} = 12.5 RU

Custom Metric Recorders

Beyond span duration, you can create custom MetricRecorders for specialized metrics.

Example: HTTP Payload Size Recorder

public class HttpPayloadSizeRecorder : IActivityListenerLogic
{
    private readonly Histogram<long> requestSizeHistogram;
    private readonly Histogram<long> responseSizeHistogram;
    
    public HttpPayloadSizeRecorder(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create("MyApp.Http");
        requestSizeHistogram = meter.CreateHistogram<long>("http.request.size", "bytes");
        responseSizeHistogram = meter.CreateHistogram<long>("http.response.size", "bytes");
    }
    
    public void ActivityStopped(Activity activity)
    {
        if (activity.Source.Name != "Microsoft.AspNetCore") return;
        
        var requestSize = activity.GetTagItem("http.request.body.size") as long? ?? 0;
        var responseSize = activity.GetTagItem("http.response.body.size") as long? ?? 0;
        
        var tags = new[]
        {
            new KeyValuePair<string, object?>("http.route", activity.GetTagItem("http.route")),
            new KeyValuePair<string, object?>("http.status_code", activity.GetTagItem("http.status_code"))
        };
        
        requestSizeHistogram.Record(requestSize, tags);
        responseSizeHistogram.Record(responseSize, tags);
    }
}

Example: Database Query Cost Recorder

public class DatabaseCostRecorder : IActivityListenerLogic
{
    private readonly Histogram<double> queryCostHistogram;
    
    public DatabaseCostRecorder(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create("MyApp.Database");
        queryCostHistogram = meter.CreateHistogram<double>("database.query.cost", "RU");
    }
    
    public void ActivityStopped(Activity activity)
    {
        if (!activity.Source.Name.StartsWith("Azure.Cosmos")) return;
        
        // Extract Request Units (RU) from Cosmos DB activity
        if (activity.GetTagItem("db.cosmosdb.request_charge") is double requestCharge)
        {
            var tags = new[]
            {
                new KeyValuePair<string, object?>("db.operation", activity.GetTagItem("db.operation")),
                new KeyValuePair<string, object?>("db.name", activity.GetTagItem("db.name"))
            };
            
            queryCostHistogram.Record(requestCharge, tags);
        }
    }
}

Registration:

services.AddSingleton<IActivityListenerLogic, HttpPayloadSizeRecorder>();
services.AddSingleton<IActivityListenerLogic, DatabaseCostRecorder>();

Complete Configuration Example

appsettings.json:

{
  "Diginsight": {
    "Activities": {
      // βœ… SHARED CONFIGURATION - applies to all metrics
      "MeterName": "MyApp.Telemetry",
      "LogBehavior": "Show",
      "LogLevel": "Debug",
      
      "ActivitySources": {
        "MyApp.*": true,
        "Azure.Cosmos.*": true,
        "Microsoft.EntityFrameworkCore": true,
        "Microsoft.AspNetCore": false,
        "System.Net.Http": true
      },
      
      "LoggedActivityNames": {
        "SmartCache.OnEvicted": "Hide",
        "Expensive.Debug.Operation": "Hide"
      },
      
      // βœ… SPAN DURATION METRIC - specific configuration
      "RecordSpanDuration": true,
      "SpanDurationMetricName": "diginsight.span_duration",
      "SpanDurationMetricDescription": "Duration of application spans",
      
      // βœ… DEFAULT FILTERS - applies to all metrics unless overridden
      "SpanMeasuredActivityNames": {
        "*": true
      },
      
      // βœ… METRIC-SPECIFIC FILTERS - override defaults per metric
      "MetricSpecificSpanMeasuredActivityNames": [
        {
          "MetricName": "diginsight.span_duration",
          "ActivityNames": {
            "MyApp.Orders.*": true,
            "MyApp.Payment.*": true,
            "MyApp.Inventory.*": true,
            "System.*": false
          }
        }
      ],
      
      // βœ… DEFAULT ENRICHER TAGS - applies to all metrics
      "MetricTags": [
        "category_name",
        "user_company"
      ],
      
      // βœ… METRIC-SPECIFIC TAGS - additional tags per metric
      "MetricSpecificTags": [
        {
          "MetricName": "diginsight.span_duration",
          "MetricTags": [
            "response_status",
            "region",
            "deployment_environment"
          ]
        }
      ]
    }
  },
  
  // Legacy filter configuration (still supported)
  "OptionsBasedMetricRecordingFilter": {
    "ActivityNames": {
      "MyApp.Orders.*": true,
      "MyApp.Payment.*": true
    }
  },
  
  // Legacy enricher configuration (still supported)
  "OptionsBasedMetricRecordingEnricher": {
    "MetricTags": [
      "customer_tier",
      "region"
    ]
  },
  
  "OpenTelemetry": {
    "Metrics": {
      "ExportIntervalMilliseconds": 5000,
      "MaxExportBatchSize": 512
    }
  }
}

Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Add Diginsight with metrics
builder.Services.AddDiginsightCore();
builder.Services.AddSpanDurationMetricRecorder();

// Add filters and enrichers
builder.Services.AddSingleton<IMetricRecordingFilter, OptionsBasedMetricRecordingFilter>();
builder.Services.AddSingleton<IMetricRecordingFilter, HttpHeadersSpanDurationMetricRecordingFilter>();
builder.Services.AddSingleton<IMetricRecordingEnricher, OptionsBasedMetricRecordingEnricher>();

// Add custom metric recorders
builder.Services.AddSingleton<IActivityListenerLogic, HttpPayloadSizeRecorder>();
builder.Services.AddSingleton<IActivityListenerLogic, DatabaseCostRecorder>();

// Configure OpenTelemetry
builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
        .AddMeter("MyApp.*")
        .AddMeter("Diginsight.Diagnostics")
        .AddRuntimeInstrumentation()
        .AddHttpClientInstrumentation()
        .AddAspNetCoreInstrumentation()
        .AddPrometheusExporter()
        .AddApplicationInsightsExporter())
    .WithTracing(tracing => tracing
        .AddSource("MyApp.*")
        .AddSource("Diginsight.Diagnostics")
        .AddHttpClientInstrumentation()
        .AddAspNetCoreInstrumentation()
        .AddApplicationInsightsExporter());

var app = builder.Build();

// Enable Prometheus scraping endpoint
app.UseOpenTelemetryPrometheusScrapingEndpoint();

app.Run();

Application Code:

public class OrderService
{
    private static readonly ActivitySource ActivitySource = new("MyApp.Orders");
    
    public async Task<Order> ProcessOrderAsync(CreateOrderRequest request)
    {
        using var activity = ActivitySource.StartRichActivity("ProcessOrder", new
        {
            customer_id = request.CustomerId,
            customer_tier = request.CustomerTier,  // Will be enriched as tag
            region = request.Region,               // Will be enriched as tag
            item_count = request.Items.Count
        });
        
        try
        {
            var order = await ValidateAndCreateOrder(request);
            activity?.SetOutput(new { order.Id, order.Status });
            return order;
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            throw;
        }
    }
}

Resulting metrics:

# Span duration with enriched tags
operation.duration{
  span_name="ProcessOrder",
  customer_tier="premium",
  region="us-east",
  status="Ok"
} = 250ms

# HTTP payload sizes
http.request.size{http.route="/api/orders", http.status_code="200"} = 1024 bytes
http.response.size{http.route="/api/orders", http.status_code="200"} = 4096 bytes

# Database costs
database.query.cost{db.operation="Query", db.name="OrdersDB"} = 12.5 RU

References

Official Documentation

  • OpenTelemetry Metrics Specification
    The official OpenTelemetry metrics specification. Essential for understanding metric types (Counter, Histogram, Gauge), semantic conventions, and best practices for metric instrumentation.

  • .NET Metrics API
    Microsoft’s documentation on System.Diagnostics.Metrics namespace. Covers Meter, Counter, Histogram creation and how Diginsight integrates with the native .NET metrics system.

  • OpenTelemetry .NET SDK
    Official OpenTelemetry .NET implementation. Shows how to configure MeterProviders, exporters (Prometheus, OTLP, Application Insights), and metric aggregation.

Monitoring Backends

  • Azure Application Insights Metrics
    Guide to querying and visualizing metrics in Application Insights. Explains how Diginsight metrics appear in Azure Monitor and how to create dashboards.

  • Prometheus Querying
    Prometheus query language (PromQL) basics. Essential for creating alerts and dashboards from Diginsight metrics exported to Prometheus.

Best Practices

  • High Cardinality Metrics Problem
    Excellent explanation of why high cardinality tags cause storage and performance issues. Critical reading for understanding why tag design matters in production.

  • OpenTelemetry Semantic Conventions
    Standard attribute naming conventions for metrics. Following these conventions ensures consistency and interoperability when metrics are sent to various backends.

  • Metric Naming Best Practices
    Industry standard for metric naming patterns. Helps design clear, consistent metric names that work well across different monitoring systems.

Diginsight Resources

  • Diginsight GitHub Repository
    Official Diginsight repository with source code, samples, and documentation. Contains working examples of metric recorders, filters, and enrichers.

  • Diginsight Samples
    Real-world sample applications demonstrating metric configuration, custom recorders, and integration with various backends.

See Also

Back to top